﻿using System.Numerics;
using ImGuiNET;
using OpenGL;
using static SDL2.Bindings.SDL;
using System.Runtime.InteropServices;
using System.Text;

namespace SDL2_OpenGL_ImGui;

public static class ImGuiImplSdl2OpenGl
{
    private static IntPtr _window = IntPtr.Zero;
    private static IntPtr _glCtx = IntPtr.Zero;
    private static Shader? _shader = null;
    private static Texture? _fontTexture = null;
    private static uint _vbo = 0, _ebo = 0, _vao = 0;
    private static ulong _time = 0;
    private static int _mouseButtonsDown = 0;
    private static List<IntPtr> _mouseCursors = null!;
    private static IntPtr _textBuffer = IntPtr.Zero;
    private static int _pendingMouseLeaveFrame = 0;
    private static IntPtr _lastMouseCursor = IntPtr.Zero;
    private static readonly IntPtr ResetRenderState = new IntPtr(-1);
    private static ulong _freq = 0;
    private const string VShader = @"
                                    #version 330 core

                                    uniform mat4 ProjMtx;
                                    layout (location = 0) in vec2 Position;
                                    layout (location = 1) in vec2 UV;
                                    layout (location = 2) in vec4 Color;
                                    out vec2 Frag_UV;
                                    out vec4 Frag_Color;

                                    void main()
                                    {
                                        Frag_UV = UV;
                                        Frag_Color = Color;
                                        gl_Position = ProjMtx * vec4(Position.xy, 0.0f, 1.0f);
                                    }";
    private const string FShader = @"
                                    #version 330 core

                                    uniform sampler2D Texture;
                                    in vec2 Frag_UV;
                                    in vec4 Frag_Color;
                                    out vec4 Out_Color;
                                    void main()
                                    {
                                        Out_Color = Frag_Color * texture(Texture, Frag_UV);
                                    }";

    private delegate void SetClipboardTextFunc(IntPtr _, IntPtr chs);
    private delegate IntPtr GetClipboardTextFunc(IntPtr _);
    private delegate void SetTextInputRectFunc(IntPtr viewport, IntPtr data);
    private delegate void UserCallback(ImDrawListPtr cmdList, ImDrawCmdPtr cmd);

    public static void Init(IntPtr window, IntPtr glCtx)
    {
        _window = window;
        _glCtx = glCtx;

        _textBuffer = Marshal.AllocHGlobal(2048);

        var io = ImGui.GetIO();

        if (io.BackendPlatformUserData != IntPtr.Zero)
            throw new Exception("ImGui already initialized a platform backend!");
        io.BackendPlatformUserData = (IntPtr) 1;
        io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors;
        io.BackendFlags |= ImGuiBackendFlags.HasSetMousePos;

        
        io.SetClipboardTextFn = Marshal.GetFunctionPointerForDelegate((SetClipboardTextFunc)SetClipboardText);
        io.GetClipboardTextFn = Marshal.GetFunctionPointerForDelegate((GetClipboardTextFunc)GetClipboardText);
        io.ClipboardUserData = IntPtr.Zero;
        io.SetPlatformImeDataFn = Marshal.GetFunctionPointerForDelegate((SetTextInputRectFunc)SetTextInputRect);

        _mouseCursors = new List<IntPtr>((int)ImGuiMouseCursor.COUNT);
        for (int i = 0; i < (int)ImGuiMouseCursor.COUNT; i++)
            _mouseCursors.Add(IntPtr.Zero);
        
        _mouseCursors[(int)ImGuiMouseCursor.Arrow] = SDL_CreateSystemCursor(SDL_SystemCursor.SDL_SYSTEM_CURSOR_ARROW);
        _mouseCursors[(int)ImGuiMouseCursor.TextInput] = SDL_CreateSystemCursor(SDL_SystemCursor.SDL_SYSTEM_CURSOR_IBEAM);
        _mouseCursors[(int)ImGuiMouseCursor.ResizeAll] = SDL_CreateSystemCursor(SDL_SystemCursor.SDL_SYSTEM_CURSOR_SIZEALL);
        _mouseCursors[(int)ImGuiMouseCursor.ResizeNS] = SDL_CreateSystemCursor(SDL_SystemCursor.SDL_SYSTEM_CURSOR_SIZENS);
        _mouseCursors[(int)ImGuiMouseCursor.ResizeEW] = SDL_CreateSystemCursor(SDL_SystemCursor.SDL_SYSTEM_CURSOR_SIZEWE);
        _mouseCursors[(int)ImGuiMouseCursor.ResizeNESW] = SDL_CreateSystemCursor(SDL_SystemCursor.SDL_SYSTEM_CURSOR_SIZENESW);
        _mouseCursors[(int)ImGuiMouseCursor.ResizeNWSE] = SDL_CreateSystemCursor(SDL_SystemCursor.SDL_SYSTEM_CURSOR_SIZENWSE);
        _mouseCursors[(int)ImGuiMouseCursor.Hand] = SDL_CreateSystemCursor(SDL_SystemCursor.SDL_SYSTEM_CURSOR_HAND);
        _mouseCursors[(int)ImGuiMouseCursor.NotAllowed] = SDL_CreateSystemCursor(SDL_SystemCursor.SDL_SYSTEM_CURSOR_NO);

        var mainViewport = ImGui.GetMainViewport();
        mainViewport.PlatformHandle = window;
        mainViewport.PlatformHandleRaw = IntPtr.Zero;

        var info = new SDL_SysWMinfo();
        SDL_VERSION(out info.version);
        switch (info.subsystem)
        {
            case SDL_SYSWM_TYPE.SDL_SYSWM_WINDOWS:
                mainViewport.PlatformHandleRaw = info.info.win.window;
                break;
            case SDL_SYSWM_TYPE.SDL_SYSWM_COCOA:
                mainViewport.PlatformHandleRaw = info.info.cocoa.window;
                break;
            case SDL_SYSWM_TYPE.SDL_SYSWM_X11:
                mainViewport.PlatformHandleRaw = info.info.x11.window;
                break;
            case SDL_SYSWM_TYPE.SDL_SYSWM_WAYLAND:
                mainViewport.PlatformHandleRaw = info.info.wl.egl_window;
                break;
        }
        
        SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
        SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1");

        if (io.BackendRendererUserData != IntPtr.Zero)
            throw new Exception("ImGui already initialized a platform backend!");
        io.BackendRendererUserData = (IntPtr) 1;
    }

    public static void Shutdown()
    {
        var io = ImGui.GetIO();
        Gl.DeleteVertexArrays(_vao);
        Gl.DeleteBuffers(_vbo);
        Gl.DeleteBuffers(_ebo);
        Marshal.FreeHGlobal(_textBuffer);
        
        io.Fonts.SetTexID(IntPtr.Zero);
        io.BackendRendererUserData = IntPtr.Zero;
        
        SDL_FreeCursor(_mouseCursors[(int)ImGuiMouseCursor.Arrow]);
        SDL_FreeCursor(_mouseCursors[(int)ImGuiMouseCursor.TextInput]);
        SDL_FreeCursor(_mouseCursors[(int)ImGuiMouseCursor.ResizeAll]);
        SDL_FreeCursor(_mouseCursors[(int)ImGuiMouseCursor.ResizeNS]);
        SDL_FreeCursor(_mouseCursors[(int)ImGuiMouseCursor.ResizeEW]);
        SDL_FreeCursor(_mouseCursors[(int)ImGuiMouseCursor.ResizeNESW]);
        SDL_FreeCursor(_mouseCursors[(int)ImGuiMouseCursor.ResizeNWSE]);
        SDL_FreeCursor(_mouseCursors[(int)ImGuiMouseCursor.Hand]);
        SDL_FreeCursor(_mouseCursors[(int)ImGuiMouseCursor.NotAllowed]);
        _lastMouseCursor = IntPtr.Zero;
        io.BackendPlatformUserData = IntPtr.Zero;
        io.BackendFlags &= ~(ImGuiBackendFlags.HasMouseCursors | ImGuiBackendFlags.HasSetMousePos | ImGuiBackendFlags.HasGamepad | ImGuiBackendFlags.PlatformHasViewports | ImGuiBackendFlags.HasMouseHoveredViewport);
    }

    public static bool ProcessEvent(ref SDL_Event e)
    {
        var io = ImGui.GetIO();
        switch (e.type)
        {
            case SDL_EventType.SDL_MOUSEMOTION:
            {
                io.AddMouseSourceEvent(e.motion.which == SDL_TOUCH_MOUSEID ? ImGuiMouseSource.TouchScreen : ImGuiMouseSource.Mouse);
                io.AddMousePosEvent(e.motion.x, e.motion.y);
                return true;
            }
            case SDL_EventType.SDL_MOUSEWHEEL:
            {
                io.AddMouseSourceEvent(e.motion.which == SDL_TOUCH_MOUSEID ? ImGuiMouseSource.TouchScreen : ImGuiMouseSource.Mouse);
                io.AddMouseWheelEvent(-e.wheel.preciseX, e.wheel.y);
                return true;
            }
            case SDL_EventType.SDL_MOUSEBUTTONDOWN:
            case SDL_EventType.SDL_MOUSEBUTTONUP:
            {
                
                var mouseButton = -1;
                if (e.button.button == SDL_BUTTON_LEFT) { mouseButton = 0; }
                if (e.button.button == SDL_BUTTON_RIGHT) { mouseButton = 1; }
                if (e.button.button == SDL_BUTTON_MIDDLE) { mouseButton = 2; }
                if (e.button.button == SDL_BUTTON_X1) { mouseButton = 3; }
                if (e.button.button == SDL_BUTTON_X2) { mouseButton = 4; }
                if (mouseButton == -1)
                    break;
                io.AddMouseSourceEvent(e.button.which == SDL_TOUCH_MOUSEID ? ImGuiMouseSource.TouchScreen : ImGuiMouseSource.Mouse);
                io.AddMouseButtonEvent(mouseButton, (e.type == SDL_EventType.SDL_MOUSEBUTTONDOWN));
                _mouseButtonsDown = (e.type == SDL_EventType.SDL_MOUSEBUTTONDOWN) ? (_mouseButtonsDown | (1 << mouseButton)) : (_mouseButtonsDown & ~(1 << mouseButton));
                return true;
            }
            case SDL_EventType.SDL_TEXTINPUT:
            {
                io.AddInputCharactersUTF8(GetStringFromEvent(ref e));
                return true;
            }
            case SDL_EventType.SDL_KEYDOWN:
            case SDL_EventType.SDL_KEYUP:
            {
                var key = e.key.keysym;
                UpdateKeyModifiers(key.mod);
                var imKeycode = KeycodeToImGuiKey(key.sym);
                io.AddKeyEvent(imKeycode, e.type == SDL_EventType.SDL_KEYDOWN);
                io.SetKeyEventNativeData(imKeycode, (int)key.sym, (int)key.scancode, (int)key.scancode);
                return true;
            }
            case SDL_EventType.SDL_WINDOWEVENT:
            {
                var windowEvent = e.window.windowEvent;
                if (windowEvent == SDL_WindowEventID.SDL_WINDOWEVENT_ENTER)
                    _pendingMouseLeaveFrame = 0;

                if (windowEvent == SDL_WindowEventID.SDL_WINDOWEVENT_LEAVE)
                    _pendingMouseLeaveFrame = ImGui.GetFrameCount() + 1;
                if (windowEvent == SDL_WindowEventID.SDL_WINDOWEVENT_FOCUS_GAINED)
                    io.AddFocusEvent(true);
                if (windowEvent == SDL_WindowEventID.SDL_WINDOWEVENT_FOCUS_LOST)
                    io.AddFocusEvent(false);
                if (windowEvent == SDL_WindowEventID.SDL_WINDOWEVENT_CLOSE ||
                    windowEvent == SDL_WindowEventID.SDL_WINDOWEVENT_MOVED ||
                    windowEvent == SDL_WindowEventID.SDL_WINDOWEVENT_RESIZED)
                {
                    var viewport = ImGui.FindViewportByPlatformHandle(SDL_GetWindowFromID(e.window.windowID));
                    if (windowEvent == SDL_WindowEventID.SDL_WINDOWEVENT_CLOSE)
                        viewport.PlatformRequestClose = true;
                    if (windowEvent == SDL_WindowEventID.SDL_WINDOWEVENT_MOVED)
                        viewport.PlatformRequestMove = true;
                    if (windowEvent == SDL_WindowEventID.SDL_WINDOWEVENT_RESIZED)
                        viewport.PlatformRequestResize = true;
                    return true;
                }
                return true;
            }
            default:
                return false;
        }
        return false;
    }
    
    public static void NewFrame()
    {
        ImGuiIOPtr io = ImGui.GetIO();
        if (_shader == null)
        {
            Gl.GetInteger(GetPName.TextureBinding2d, out uint lastTex);
            Gl.GetInteger(GetPName.ArrayBufferBinding, out uint lastVbo);
            Gl.GetInteger(GetPName.ElementArrayBufferBinding, out uint lastEbo);
            Gl.GetInteger(GetPName.VertexArrayBinding, out uint lastVao);

            _shader = new Shader("__imgui_inner_shader", VShader, FShader);
            _shader.Build();
            
            _vao = Gl.GenVertexArray();
            _vbo = Gl.GenBuffer();
            _ebo = Gl.GenBuffer();
            Gl.BindVertexArray(_vao);
            Gl.BindBuffer(BufferTarget.ArrayBuffer, _vbo);
            Gl.BindBuffer(BufferTarget.ElementArrayBuffer, _ebo);
        
            io.Fonts.GetTexDataAsRGBA32(out IntPtr pixels, out var width, out var height, out _);
            Gl.GetInteger(GetPName.TextureBinding2d, out uint prevTex2d);
            _fontTexture = new Texture(pixels, width, height);
            io.Fonts.SetTexID((IntPtr)_fontTexture.Id);
            Gl.BindTexture(TextureTarget.Texture2d, prevTex2d);
            io.Fonts.ClearTexData();
        
            Gl.BindTexture(TextureTarget.Texture2d, lastTex);
            Gl.BindVertexArray(lastVao);
            Gl.BindBuffer(BufferTarget.ArrayBuffer, lastVbo);
            Gl.BindBuffer(BufferTarget.ElementArrayBuffer, lastEbo);
            _freq = SDL_GetPerformanceFrequency();
        }
        
        SDL_GetWindowSize(_window, out var winW, out var winH);
        if ((SDL_GetWindowFlags(_window) & (uint)SDL_WindowFlags.SDL_WINDOW_MINIMIZED) != 0)
            winW = winH = 0;
        SDL_GL_GetDrawableSize(_window, out var displayW, out var displayH);
        io.DisplaySize = new Vector2(winW, winH);
        if (winW > 0 && winH > 0)
            io.DisplayFramebufferScale = new Vector2((float) displayW / winW, (float) displayH / winH);
        var currentTime = SDL_GetPerformanceCounter();
        if (currentTime < _time)
            currentTime = _time + 1;
        io.DeltaTime = _time > 0 ? (float)((double)(currentTime - _time) / _freq) : (1.0f / 60.0f);
        _time = currentTime;
        
        if (_pendingMouseLeaveFrame != 0 && _pendingMouseLeaveFrame >= ImGui.GetFrameCount() && _mouseButtonsDown == 0)
        {
            _pendingMouseLeaveFrame = 0;
            io.AddMousePosEvent(-float.MaxValue, -float.MaxValue);
        }
        
        var isAppFocused = (SDL_GetWindowFlags(_window) & (uint)SDL_WindowFlags.SDL_WINDOW_INPUT_FOCUS) != 0; // SDL 2.0.3 and non-windowed systems: single-viewport only
        if (isAppFocused)
            if (io.WantSetMousePos)
                SDL_WarpMouseInWindow(_window, (int)io.MousePos.X, (int)io.MousePos.Y);

        if ((io.ConfigFlags & ImGuiConfigFlags.NoMouseCursorChange) == 0)
        {
            var cursor = ImGui.GetMouseCursor();
            if (io.MouseDrawCursor || cursor == ImGuiMouseCursor.None)
            {
                SDL_ShowCursor(0);  // 0 for sdl_false
            }
            else
            {
                var expectedCursor = _mouseCursors[(int) cursor] != IntPtr.Zero
                    ? _mouseCursors[(int) cursor]
                    : _mouseCursors[(int) ImGuiMouseCursor.Arrow];
                if (_lastMouseCursor != expectedCursor)
                {
                    SDL_SetCursor(expectedCursor);
                    _lastMouseCursor = expectedCursor;
                }

                SDL_ShowCursor(1);  // 1 for sdl_true
            }
        }
    }
    
    public static void Render(ImDrawDataPtr drawData)
    {
        int fbWidth = (int)(drawData.DisplaySize.X * drawData.FramebufferScale.X);
        int fbHeight = (int)(drawData.DisplaySize.Y * drawData.FramebufferScale.Y);
        if (fbWidth <= 0 || fbHeight <= 0)
            return;

        // Backup GL state
        Gl.GetInteger(GetPName.ActiveTexture, out TextureUnit lastTex);
        Gl.GetInteger(GetPName.CurrentProgram, out uint lastShader);
        Gl.GetInteger(GetPName.Texture2d, out uint lastTex2d);
        Gl.GetInteger(GetPName.SamplerBinding, out uint lastSample);
        Gl.GetInteger(GetPName.ArrayBufferBinding, out uint lastVbo);
        Gl.GetInteger(GetPName.VertexArrayBinding, out uint lastVao);
        Gl.GetInteger(GetPName.ElementArrayBufferBinding, out uint lastEbo);
        var lastPolygonMode = new PolygonMode[2];
        Gl.GetInteger(GetPName.PolygonMode, 2, out lastPolygonMode[0]);
        var lastViewport = new int[4];
        Gl.GetInteger(GetPName.Viewport, 4,  out lastViewport[0]);
        var lastScissorBox = new int[4];
        Gl.GetInteger(GetPName.ScissorBox, 4,  out lastScissorBox[0]);
        Gl.GetInteger(GetPName.BlendSrcRgb, out BlendingFactor lastBlendFuncSrcRgb);
        Gl.GetInteger(GetPName.BlendDstRgb, out BlendingFactor lastBlendFuncDstRgb);
        Gl.GetInteger(GetPName.BlendSrcAlpha, out BlendingFactor lastBlendFuncSrcAlpha);
        Gl.GetInteger(GetPName.BlendDstAlpha, out BlendingFactor lastBlendFuncDstAlpha);
        Gl.GetInteger(GetPName.BlendEquationRgb, out BlendEquationMode lastBlendEquationRgb);
        Gl.GetInteger(GetPName.BlendEquationAlpha, out BlendEquationMode lastBlendEquationAlpha);
        
        var lastBlendEnabled = Gl.IsEnabled(EnableCap.Blend);
        var lastCullFaceEnabled = Gl.IsEnabled(EnableCap.CullFace);
        var lastDepthTestEnabled = Gl.IsEnabled(EnableCap.DepthTest);
        var lastStencilTestEnabled = Gl.IsEnabled(EnableCap.StencilTest);
        var lastScissorTestEnabled = Gl.IsEnabled(EnableCap.ScissorTest);
        var lastPrimitiveRestartEnabled = Gl.IsEnabled(EnableCap.PrimitiveRestart);
        
        Gl.ActiveTexture(TextureUnit.Texture0);
        
        SetupRenderState(fbWidth, fbHeight);
        
        // Will project scissor/clipping rectangles into framebuffer space
        var clipOff = drawData.DisplayPos;         // (0,0) unless using multi-viewports
        var clipScale = drawData.FramebufferScale; // (1,1) unless using retina display which are often (2,2)
        for (var i = 0; i < drawData.CmdListsCount; i++)
        {
            var cmdList = drawData.CmdListsRange[i];
            uint vertexSize, indexSize;
            unsafe
            {
                vertexSize = (uint)(cmdList.VtxBuffer.Size * sizeof(ImDrawVert));
                indexSize = (uint)(cmdList.IdxBuffer.Size * sizeof(ushort));
            }
            Gl.BindVertexArray(_vao);
            Gl.BindBuffer(BufferTarget.ArrayBuffer, _vbo);
            Gl.BindBuffer(BufferTarget.ElementArrayBuffer, _ebo);
            Gl.BufferData(BufferTarget.ArrayBuffer, vertexSize, cmdList.VtxBuffer.Data, BufferUsage.StreamDraw);
            Gl.BufferData(BufferTarget.ElementArrayBuffer, indexSize, cmdList.IdxBuffer.Data, BufferUsage.StreamDraw);
        
        
            for (var j = 0; j < cmdList.CmdBuffer.Size; j++)
            {
                var cmd = cmdList.CmdBuffer[j];
                if (cmd.UserCallback != IntPtr.Zero)
                {
                    if (cmd.UserCallback == ResetRenderState)
                        SetupRenderState(fbWidth, fbHeight);
                    else
                        Marshal.GetDelegateForFunctionPointer<UserCallback>(cmd.UserCallback)(cmdList, cmd);
                }
                else
                {
                    var clipMin = new Vector2((cmd.ClipRect.X - clipOff.X) * clipScale.X, (cmd.ClipRect.Y - clipOff.Y) * clipScale.Y);
                    var clipMax = new Vector2((cmd.ClipRect.Z - clipOff.X) * clipScale.X, (cmd.ClipRect.W - clipOff.Y) * clipScale.Y);
                    if (clipMax.X <= clipMin.X || clipMax.Y <= clipMin.Y)
                        continue;
                    Gl.Scissor((int)clipMin.X, (int)(fbHeight - clipMax.Y), (int)(clipMax.X - clipMin.X), (int)(clipMax.Y - clipMin.Y));
                    Gl.BindTexture(TextureTarget.Texture2d, (uint)cmd.TextureId);
                    Gl.DrawElements(PrimitiveType.Triangles, (int)cmd.ElemCount, DrawElementsType.UnsignedShort, (int)cmd.IdxOffset * sizeof(ushort));
                }
            }
        }
        
        Gl.UseProgram(lastShader);
        Gl.BindTexture(TextureTarget.Texture2d, lastTex2d);
        Gl.BindSampler(0, lastSample);
        Gl.ActiveTexture(lastTex);
        Gl.BindVertexArray(lastVao);
        Gl.BindBuffer(BufferTarget.ArrayBuffer, lastVbo);
        Gl.BindBuffer(BufferTarget.ElementArrayBuffer, lastEbo);
        Gl.BlendEquationSeparate(lastBlendEquationRgb, lastBlendEquationAlpha);
        Gl.BlendFuncSeparate(lastBlendFuncSrcRgb, lastBlendFuncDstRgb, lastBlendFuncSrcAlpha, lastBlendFuncDstAlpha);
        if (lastBlendEnabled) Gl.Enable(EnableCap.Blend); else Gl.Disable(EnableCap.Blend);
        if (lastCullFaceEnabled) Gl.Enable(EnableCap.CullFace); else Gl.Disable(EnableCap.CullFace);
        if (lastDepthTestEnabled) Gl.Enable(EnableCap.DepthTest); else Gl.Disable(EnableCap.DepthTest);
        if (lastStencilTestEnabled) Gl.Enable(EnableCap.StencilTest); else Gl.Disable(EnableCap.StencilTest);
        if (lastScissorTestEnabled) Gl.Enable(EnableCap.ScissorTest); else Gl.Disable(EnableCap.ScissorTest);
        if (lastPrimitiveRestartEnabled) Gl.Enable(EnableCap.PrimitiveRestart); else Gl.Disable(EnableCap.PrimitiveRestart);
        Gl.PolygonMode(MaterialFace.FrontAndBack, lastPolygonMode[0]);
        
        Gl.Viewport(lastViewport[0], lastViewport[1], lastViewport[2], lastViewport[3]);
        Gl.Scissor(lastScissorBox[0], lastScissorBox[1], lastScissorBox[2], lastScissorBox[3]);
    }

    private static void SetClipboardText(IntPtr _, IntPtr chs)
    {
        var str = Marshal.PtrToStringUTF8(chs);
        SDL_SetClipboardText(str ?? string.Empty);
    }

    private static IntPtr GetClipboardText(IntPtr _)
    {
        var t = SDL_GetClipboardText() + "\0";
        var bytes = Encoding.UTF8.GetBytes(t);
        if (Marshal.SizeOf(_textBuffer) < bytes.Length)
        {
            Marshal.FreeHGlobal(_textBuffer);
            _textBuffer = Marshal.AllocHGlobal(bytes.Length);
        }
        Marshal.Copy(bytes, 0, _textBuffer, bytes.Length);
        return _textBuffer;
    }

    private static void SetTextInputRect(IntPtr viewportPtr, IntPtr dataPtr)
    {
        var viewport = Marshal.PtrToStructure<ImGuiViewport>(viewportPtr);
        var data = Marshal.PtrToStructure<ImGuiPlatformImeData>(dataPtr);
        if (data.WantVisible == 0) return;
        var rect = new SDL_Rect
        {
            x = (int)(data.InputPos.X - viewport.Pos.X),
            y = (int)(data.InputPos.Y - viewport.Pos.Y + data.InputLineHeight),
            w = 1,
            h = (int)data.InputLineHeight
        };
        SDL_SetTextInputRect(ref rect);
    }

    private static unsafe string GetStringFromEvent(ref SDL_Event e)
    {
        fixed (byte* b = e.text.text)
        {
            return Marshal.PtrToStringUTF8((IntPtr)b) ?? "";
        }
    }

    private static void UpdateKeyModifiers(SDL_Keymod keyMods)
    {
        var io = ImGui.GetIO();
        io.AddKeyEvent(ImGuiKey.ModCtrl, (keyMods & SDL_Keymod.KMOD_CTRL) != 0);
        io.AddKeyEvent(ImGuiKey.ModShift, (keyMods & SDL_Keymod.KMOD_SHIFT) != 0);
        io.AddKeyEvent(ImGuiKey.ModAlt, (keyMods & SDL_Keymod.KMOD_ALT) != 0);
        io.AddKeyEvent(ImGuiKey.ModSuper, (keyMods & SDL_Keymod.KMOD_GUI) != 0);
    }

    private static ImGuiKey KeycodeToImGuiKey(SDL_Keycode keycode)
    {
        switch (keycode)
        {
            case SDL_Keycode.SDLK_TAB: return ImGuiKey.Tab;
            case SDL_Keycode.SDLK_LEFT: return ImGuiKey.LeftArrow;
            case SDL_Keycode.SDLK_RIGHT: return ImGuiKey.RightArrow;
            case SDL_Keycode.SDLK_UP: return ImGuiKey.UpArrow;
            case SDL_Keycode.SDLK_DOWN: return ImGuiKey.DownArrow;
            case SDL_Keycode.SDLK_PAGEUP: return ImGuiKey.PageUp;
            case SDL_Keycode.SDLK_PAGEDOWN: return ImGuiKey.PageDown;
            case SDL_Keycode.SDLK_HOME: return ImGuiKey.Home;
            case SDL_Keycode.SDLK_END: return ImGuiKey.End;
            case SDL_Keycode.SDLK_INSERT: return ImGuiKey.Insert;
            case SDL_Keycode.SDLK_DELETE: return ImGuiKey.Delete;
            case SDL_Keycode.SDLK_BACKSPACE: return ImGuiKey.Backspace;
            case SDL_Keycode.SDLK_SPACE: return ImGuiKey.Space;
            case SDL_Keycode.SDLK_RETURN: return ImGuiKey.Enter;
            case SDL_Keycode.SDLK_ESCAPE: return ImGuiKey.Escape;
            case SDL_Keycode.SDLK_QUOTE: return ImGuiKey.Apostrophe;
            case SDL_Keycode.SDLK_COMMA: return ImGuiKey.Comma;
            case SDL_Keycode.SDLK_MINUS: return ImGuiKey.Minus;
            case SDL_Keycode.SDLK_PERIOD: return ImGuiKey.Period;
            case SDL_Keycode.SDLK_SLASH: return ImGuiKey.Slash;
            case SDL_Keycode.SDLK_SEMICOLON: return ImGuiKey.Semicolon;
            case SDL_Keycode.SDLK_EQUALS: return ImGuiKey.Equal;
            case SDL_Keycode.SDLK_LEFTBRACKET: return ImGuiKey.LeftBracket;
            case SDL_Keycode.SDLK_BACKSLASH: return ImGuiKey.Backslash;
            case SDL_Keycode.SDLK_RIGHTBRACKET: return ImGuiKey.RightBracket;
            case SDL_Keycode.SDLK_BACKQUOTE: return ImGuiKey.GraveAccent;
            case SDL_Keycode.SDLK_CAPSLOCK: return ImGuiKey.CapsLock;
            case SDL_Keycode.SDLK_SCROLLLOCK: return ImGuiKey.ScrollLock;
            case SDL_Keycode.SDLK_NUMLOCKCLEAR: return ImGuiKey.NumLock;
            case SDL_Keycode.SDLK_PRINTSCREEN: return ImGuiKey.PrintScreen;
            case SDL_Keycode.SDLK_PAUSE: return ImGuiKey.Pause;
            case SDL_Keycode.SDLK_KP_0: return ImGuiKey.Keypad0;
            case SDL_Keycode.SDLK_KP_1: return ImGuiKey.Keypad1;
            case SDL_Keycode.SDLK_KP_2: return ImGuiKey.Keypad2;
            case SDL_Keycode.SDLK_KP_3: return ImGuiKey.Keypad3;
            case SDL_Keycode.SDLK_KP_4: return ImGuiKey.Keypad4;
            case SDL_Keycode.SDLK_KP_5: return ImGuiKey.Keypad5;
            case SDL_Keycode.SDLK_KP_6: return ImGuiKey.Keypad6;
            case SDL_Keycode.SDLK_KP_7: return ImGuiKey.Keypad7;
            case SDL_Keycode.SDLK_KP_8: return ImGuiKey.Keypad8;
            case SDL_Keycode.SDLK_KP_9: return ImGuiKey.Keypad9;
            case SDL_Keycode.SDLK_KP_PERIOD: return ImGuiKey.KeypadDecimal;
            case SDL_Keycode.SDLK_KP_DIVIDE: return ImGuiKey.KeypadDivide;
            case SDL_Keycode.SDLK_KP_MULTIPLY: return ImGuiKey.KeypadMultiply;
            case SDL_Keycode.SDLK_KP_MINUS: return ImGuiKey.KeypadSubtract;
            case SDL_Keycode.SDLK_KP_PLUS: return ImGuiKey.KeypadAdd;
            case SDL_Keycode.SDLK_KP_ENTER: return ImGuiKey.KeypadEnter;
            case SDL_Keycode.SDLK_KP_EQUALS: return ImGuiKey.KeypadEqual;
            case SDL_Keycode.SDLK_LCTRL: return ImGuiKey.LeftCtrl;
            case SDL_Keycode.SDLK_LSHIFT: return ImGuiKey.LeftShift;
            case SDL_Keycode.SDLK_LALT: return ImGuiKey.LeftAlt;
            case SDL_Keycode.SDLK_LGUI: return ImGuiKey.LeftSuper;
            case SDL_Keycode.SDLK_RCTRL: return ImGuiKey.RightCtrl;
            case SDL_Keycode.SDLK_RSHIFT: return ImGuiKey.RightShift;
            case SDL_Keycode.SDLK_RALT: return ImGuiKey.RightAlt;
            case SDL_Keycode.SDLK_RGUI: return ImGuiKey.RightSuper;
            case SDL_Keycode.SDLK_APPLICATION: return ImGuiKey.Menu;
            case SDL_Keycode.SDLK_0: return ImGuiKey._0;
            case SDL_Keycode.SDLK_1: return ImGuiKey._1;
            case SDL_Keycode.SDLK_2: return ImGuiKey._2;
            case SDL_Keycode.SDLK_3: return ImGuiKey._3;
            case SDL_Keycode.SDLK_4: return ImGuiKey._4;
            case SDL_Keycode.SDLK_5: return ImGuiKey._5;
            case SDL_Keycode.SDLK_6: return ImGuiKey._6;
            case SDL_Keycode.SDLK_7: return ImGuiKey._7;
            case SDL_Keycode.SDLK_8: return ImGuiKey._8;
            case SDL_Keycode.SDLK_9: return ImGuiKey._9;
            case SDL_Keycode.SDLK_a: return ImGuiKey.A;
            case SDL_Keycode.SDLK_b: return ImGuiKey.B;
            case SDL_Keycode.SDLK_c: return ImGuiKey.C;
            case SDL_Keycode.SDLK_d: return ImGuiKey.D;
            case SDL_Keycode.SDLK_e: return ImGuiKey.E;
            case SDL_Keycode.SDLK_f: return ImGuiKey.F;
            case SDL_Keycode.SDLK_g: return ImGuiKey.G;
            case SDL_Keycode.SDLK_h: return ImGuiKey.H;
            case SDL_Keycode.SDLK_i: return ImGuiKey.I;
            case SDL_Keycode.SDLK_j: return ImGuiKey.J;
            case SDL_Keycode.SDLK_k: return ImGuiKey.K;
            case SDL_Keycode.SDLK_l: return ImGuiKey.L;
            case SDL_Keycode.SDLK_m: return ImGuiKey.M;
            case SDL_Keycode.SDLK_n: return ImGuiKey.N;
            case SDL_Keycode.SDLK_o: return ImGuiKey.O;
            case SDL_Keycode.SDLK_p: return ImGuiKey.P;
            case SDL_Keycode.SDLK_q: return ImGuiKey.Q;
            case SDL_Keycode.SDLK_r: return ImGuiKey.R;
            case SDL_Keycode.SDLK_s: return ImGuiKey.S;
            case SDL_Keycode.SDLK_t: return ImGuiKey.T;
            case SDL_Keycode.SDLK_u: return ImGuiKey.U;
            case SDL_Keycode.SDLK_v: return ImGuiKey.V;
            case SDL_Keycode.SDLK_w: return ImGuiKey.W;
            case SDL_Keycode.SDLK_x: return ImGuiKey.X;
            case SDL_Keycode.SDLK_y: return ImGuiKey.Y;
            case SDL_Keycode.SDLK_z: return ImGuiKey.Z;
            case SDL_Keycode.SDLK_F1: return ImGuiKey.F1;
            case SDL_Keycode.SDLK_F2: return ImGuiKey.F2;
            case SDL_Keycode.SDLK_F3: return ImGuiKey.F3;
            case SDL_Keycode.SDLK_F4: return ImGuiKey.F4;
            case SDL_Keycode.SDLK_F5: return ImGuiKey.F5;
            case SDL_Keycode.SDLK_F6: return ImGuiKey.F6;
            case SDL_Keycode.SDLK_F7: return ImGuiKey.F7;
            case SDL_Keycode.SDLK_F8: return ImGuiKey.F8;
            case SDL_Keycode.SDLK_F9: return ImGuiKey.F9;
            case SDL_Keycode.SDLK_F10: return ImGuiKey.F10;
            case SDL_Keycode.SDLK_F11: return ImGuiKey.F11;
            case SDL_Keycode.SDLK_F12: return ImGuiKey.F12;
        }
        return ImGuiKey.None;
    }

    private static void SetupRenderState(int fbWidth, int fbHeight)
    {
        var drawData = ImGui.GetDrawData();
        Gl.Enable(EnableCap.Blend);
        Gl.BlendEquation(BlendEquationMode.FuncAdd);
        Gl.BlendFuncSeparate(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha, BlendingFactor.One, BlendingFactor.OneMinusSrcAlpha);
        Gl.Disable(EnableCap.CullFace);
        Gl.Disable(EnableCap.DepthTest);
        Gl.Disable(EnableCap.StencilTest);
        Gl.Enable(EnableCap.ScissorTest);
        Gl.Disable(EnableCap.PrimitiveRestart);
        Gl.PolygonMode(MaterialFace.FrontAndBack, PolygonMode.Fill);

        Gl.Viewport(0, 0, fbWidth, fbHeight);
        var l = drawData.DisplayPos.X;
        var r = drawData.DisplayPos.X + drawData.DisplaySize.X;
        var t = drawData.DisplayPos.Y;
        var b = drawData.DisplayPos.Y + drawData.DisplaySize.Y; 
        var orthoProjection = new Matrix4x4
        (
            2.0f/(r-l),   0.0f,         0.0f,   0.0f,
            0.0f,         2.0f/(t-b),   0.0f,   0.0f,
            0.0f,         0.0f,        -1.0f,   0.0f,
            (r+l)/(l-r),  (t+b)/(b-t),  0.0f,   1.0f
        );
        var s = _shader ?? throw new Exception("ImGui shader not init.");
        s.UseShader();
        s.SetInt("Texture", 0);
        s.SetMat4("ProjMtx", orthoProjection);
        
        Gl.BindSampler(0, 0);
        
        Gl.BindVertexArray(_vao);
        Gl.BindBuffer(BufferTarget.ArrayBuffer, _vbo);
        Gl.BindBuffer(BufferTarget.ElementArrayBuffer, _ebo);
        
        Gl.EnableVertexAttribArray(0);
        Gl.EnableVertexAttribArray(1);
        Gl.EnableVertexAttribArray(2);

        unsafe
        {
            Gl.VertexAttribPointer(0, 2, VertexAttribType.Float, false, sizeof(ImDrawVert), 0);
            Gl.VertexAttribPointer(1, 2, VertexAttribType.Float, false, sizeof(ImDrawVert), 2 * sizeof(float));
            Gl.VertexAttribPointer(2, 4, VertexAttribType.UnsignedByte, true, sizeof(ImDrawVert), 4 * sizeof(float));
        }

    }

}